/* 
 * File:   VoxelGrid.cpp
 * Author: brady
 * 
 * Created on August 28, 2011, 9:41 PM
 */

#include <acceleration/VoxelGrid.h>

VoxelGrid::VoxelGrid() {
    shapeCount = 0;
    holdingStructure = BoundingVolumeHierarchy();
}

VoxelGrid::VoxelGrid(const VoxelGrid& orig) {
}

VoxelGrid::~VoxelGrid() {
}

void VoxelGrid::addShape(Shape* shape){
    holdingStructure.addShape(shape);
    shapeCount++;
}
void VoxelGrid::finalize(){

    holdingStructure.finalize();
    minCorner = holdingStructure.getBoundingBox().getMin();
    maxCorner = holdingStructure.getBoundingBox().getMax();

    double gridX = maxCorner[0] - minCorner[0];
    double gridY = maxCorner[1] - minCorner[1];
    double gridZ = maxCorner[2] - minCorner[2];
    
    int divs = (int)(pow(shapeCount, 1.0/3.0) + 1.0);
    voxelDimensions = Vector3D(gridX/(double)divs, gridY/(double)divs, gridZ/(double)divs);
    
    xCount = divs;
    yCount = divs;
    zCount = divs;
        
    //cout << "Allocating voxels...\n";
    voxels = new Voxel***[xCount];
    for(int x = 0; x<xCount; x++){
        voxels[x] = new Voxel**[yCount];
        for(int y =0; y<yCount; y++){
            voxels[x][y] = new Voxel*[zCount];
            for (int z = 0; z<zCount; z++){
                Vector3D vMin(minCorner);
                vMin[0] += voxelDimensions[0] * x;
                vMin[1] += voxelDimensions[1] * y;
                vMin[2] += voxelDimensions[2] * z;
                Vector3D vMax = vMin + voxelDimensions;
                Voxel* cur = new Voxel(vMin, vMax);
                cur->x = x;
                cur->y = y;
                cur->z = z;
                voxels[x][y][z] = cur;
                
            }
        }
    }
    //cout << "done\n";
    //cout << "adding " << shapeCount << " shapes to " << xCount*yCount*zCount << " voxels...\n";
    int shapeRefs = 0;
    int maxRefs = 0;
    
    for (int x = 0; x < xCount; x++) {
        for (int y = 0; y < yCount; y++) {
            for (int z = 0; z < zCount; z++) {
                vector<Shape*> overlap = holdingStructure.getBoundingBoxIntersections(voxels[x][y][z]->boundingBox);
                voxels[x][y][z]->addShapes(overlap);
                maxRefs = max(maxRefs, (int)overlap.size());
                shapeRefs += overlap.size();
            }
        }
    }
    holdingStructure.~BoundingVolumeHierarchy();
    //cout << "total shape references: " << shapeRefs <<endl;
    //cout << "average shapes per voxel: " << shapeRefs/(double)(xCount*yCount*zCount) << endl;
    //cout << "highest voxel capacity: " << maxRefs << endl;
    //cout << "done\n";
}

IntersectionResult VoxelGrid::getFirstIntersection(IntersectionParams& ip){
    vector<Voxel*> hitVoxels = getIntersectedVoxels(ip);
    cout.flush();
    for(int i = 0; i<hitVoxels.size(); i++){
        IntersectionResult cur_ir = hitVoxels[i]->shapes->getFirstIntersection(ip);
        if(cur_ir.shape != NULL && hitVoxels[i]->boundingBox.contains(cur_ir.hitPoint)){
            return cur_ir;
        }
    }
    IntersectionResult noHit;
    noHit.shape = NULL;
    noHit.tVal = DBL_MAX;
    return noHit;
}

vector<IntersectionResult> VoxelGrid::getAllIntersections(IntersectionParams& ip){
    vector<Voxel*> hitVoxels = getIntersectedVoxels(ip);
    vector<IntersectionResult> results;
    for(int i = 0; i<hitVoxels.size(); i++){
        vector<IntersectionResult> v_res = hitVoxels[i]->shapes->getAllIntersections(ip);
        for(int j = 0; j < v_res.size(); j++){
            if(v_res[j].shape != NULL && hitVoxels[i]->boundingBox.contains(v_res[j].hitPoint))
            results.push_back(v_res[j]);
        }
    }
    return results;
}

IntersectionResult VoxelGrid::getAnyIntersection(IntersectionParams& ip){
    return getFirstIntersection(ip);
}

Voxel* VoxelGrid::getVoxel(Vector3D point){
    //get vector offset from minCorner
    Vector3D local = point - minCorner;
    
    if(local[0] < 0 || local[1] < 0 || local[2] < 0){
        return NULL;
    }
    
    int vx = (int)(local[0]/voxelDimensions[0]);
    int vy = (int)(local[1]/voxelDimensions[1]);
    int vz = (int)(local[2]/voxelDimensions[2]);

    if( vx < 0 || vx >= xCount ||
        vy < 0 || vy >= yCount ||
        vz < 0 || vz >= zCount   ) return NULL;

    return voxels[vx][vy][vz];
}

Voxel* VoxelGrid::getVoxel(int x, int y, int z){
    if(x >= xCount || x < 0) return NULL;
    if(y >= yCount || y < 0) return NULL;
    if(z >= zCount || z < 0) return NULL;
    return voxels[x][y][z];
    
}

vector<Voxel*> VoxelGrid::getIntersectedVoxels(IntersectionParams& ip){
    vector<Voxel*> hitVoxels = vector<Voxel*>();
    const Ray& ray = ip.getRay();
    Voxel* curVoxel = getVoxel(ray.follow(ip.getTMin()));
    if(curVoxel == NULL){
        double* t_enter = new double;
        double* t_exit = new double;
        BoundingBox gridBounds(minCorner, maxCorner);
        if(gridBounds.testIntersect(ip, t_enter, t_exit)){
            double offset = (*t_enter < 0)? -FLT_EPSILON : FLT_EPSILON;
            curVoxel = getVoxel(ray.follow(*t_enter + offset));
            delete t_enter;
            delete t_exit;
        }
        else { 
            delete t_enter;
            delete t_exit;
            return hitVoxels;
        }
    }
    
    while(curVoxel != NULL){
        hitVoxels.push_back(curVoxel);
        curVoxel = getNextVoxel(curVoxel, ip);
    }

    return hitVoxels;
}

Voxel* VoxelGrid::getNextVoxel(Voxel* curVoxel, IntersectionParams& ip){
    if(curVoxel == NULL) return NULL;
    const Ray& ray = ip.getRay();
    int x = curVoxel->x;
    int y = curVoxel->y;
    int z = curVoxel->z;
    double tMinX, tMaxX, tMinY, tMaxY, tMinZ, tMaxZ;
    int stepX = (ray.getDirection()[0] > 0? 1 : -1);
    int stepY = (ray.getDirection()[1] > 0? 1 : -1);
    int stepZ = (ray.getDirection()[2] > 0? 1 : -1);

    if(stepX == 1){
        tMaxX = (curVoxel->boundingBox.getMax()[0] - ray.getOrigin()[0])/ray.getDirection()[0] ;
        tMinX = (curVoxel->boundingBox.getMin()[0] - ray.getOrigin()[0])/ray.getDirection()[0] ;
    }
    else{
        tMinX = (curVoxel->boundingBox.getMax()[0] - ray.getOrigin()[0])/ray.getDirection()[0] ;
        tMaxX = (curVoxel->boundingBox.getMin()[0] - ray.getOrigin()[0])/ray.getDirection()[0] ;
    }

    if(stepY == 1){
        tMaxY = (curVoxel->boundingBox.getMax()[1] - ray.getOrigin()[1])/ray.getDirection()[1] ;
        tMinY = (curVoxel->boundingBox.getMin()[1] - ray.getOrigin()[1])/ray.getDirection()[1] ;
    }
    else{
        tMinY = (curVoxel->boundingBox.getMax()[1] - ray.getOrigin()[1])/ray.getDirection()[1] ;
        tMaxY = (curVoxel->boundingBox.getMin()[1] - ray.getOrigin()[1])/ray.getDirection()[1] ;
    }
    
    if(stepZ == 1){
        tMaxZ = (curVoxel->boundingBox.getMax()[2] - ray.getOrigin()[2])/ray.getDirection()[2] ;
        tMinZ = (curVoxel->boundingBox.getMin()[2] - ray.getOrigin()[2])/ray.getDirection()[2] ;
    }
    else{
        tMinZ = (curVoxel->boundingBox.getMax()[2] - ray.getOrigin()[2])/ray.getDirection()[2] ;
        tMaxZ = (curVoxel->boundingBox.getMin()[2] - ray.getOrigin()[2])/ray.getDirection()[2] ;
    }
    
    double tMax = min(min(tMaxX, tMaxY), tMaxZ);
    double tMin = max(max(tMinX, tMinY), tMinZ);
    
    if(tMax < tMin) return NULL; //we missed
    
    if(tMaxX < tMaxY && tMaxX < tMaxZ){
        return getVoxel(x+stepX, y, z);
    }
    
    if(tMaxY < tMaxX && tMaxY < tMaxZ){
        return getVoxel(x, y+stepY, z);
    }
    
    else{
        return getVoxel(x, y, z+stepZ);
    } 
}